﻿/* HEADER
 * ------
 * © 2009 by Salomon Zwecker 
 * modified by:
 * - 
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using System.Collections.ObjectModel;

namespace Shapes.Geometry
{
    /// <summary>
    /// A PointList is an abstract class to manage connected points.
    /// LineStrip and BSpline inherits it
    /// </summary>
    /// <typeparam name="T">The type of the points</typeparam>
    public abstract class PointList<T>  : Drawing
        where T : IEquatable<T>
    {
        internal List<T> _Points;
        internal List<Line> _Lines = new List<Line>();
        /// <summary>
        /// The connection of all points with straight Lines.
        /// </summary>
        public ReadOnlyCollection<Line> Lines { get { return _Lines.AsReadOnly(); } }

        /// <summary>
        /// It is closed when Close() has been called or a shape (e.g. Polygon) has been created from it
        /// </summary>
        public bool IsClosed { get; internal set; }

        ///////////////////////////////////////////////////////////
        // CONSTRUCTOR
        /// <summary>
        /// Creates a new instance of PointList
        /// </summary>
        /// <param name="points">the ordered List of corners</param>
        protected PointList(params T[] points)
            : base()
        {
            _Points = new List<T>();

            foreach (T p in points)
                _Points.Add(p);

            if (_Points.Count < 3)
                throw new TooFewPointsException("A PointList must have at least 3 corners!");

            SetupLines();
        }

        internal void SetupLines()
        {
            _Lines.Clear();

            for (int i = 0; i < _Points.Count - 1; i++)
            {
                _Lines.Add(
                    new Line(
                        GetVectorOfPoint(_Points[i]),
                        GetVectorOfPoint(_Points[i + 1])
                    ));
            }
        }

        ///////////////////////////////////////////////////////////
        // ABSTRACT
        /// <summary>
        /// Closes the PointList
        /// </summary>
        public abstract void Close();
        /// <summary>
        /// converts the given position to local coordinates
        /// </summary>
        /// <param name="globalPosition">the global position</param>
        /// <returns>the local position</returns>
        public abstract Vector2 GetLocalPositionOfPoint(T globalPosition);
        /// <summary>
        /// Gets the local position of the point with the given index
        /// </summary>
        /// <param name="index">the index of the point to retrieve</param>
        /// <returns>the point at the given index in local space</returns>
        public T GetLocalPoint(int index)
        {
            return _Points[index % _Points.Count];
        }
        /// <summary>
        /// converts the given position to global coordinates
        /// </summary>
        /// <param name="localPosition">the local position</param>
        /// <returns>the local position</returns>
        public abstract Vector2 GetGlobalPositionOfPoint(T localPosition);
        /// <summary>
        /// Returns the given point as a Vector2
        /// </summary>
        /// <param name="point">the point</param>
        /// <returns>the vector of the point</returns>
        protected abstract Vector2 GetVectorOfPoint(T point);
        /// <summary>
        /// fits the bounding
        /// </summary>
        internal abstract void FitBounding();
        /// <summary>
        /// converts the given position to local coordinates
        /// </summary>
        protected abstract void ToLocalCoordinates(ref T outPoint);
        /// <summary>
        /// converts the given position to global coordinates
        /// </summary>
        protected abstract void ToGlobalCoordinates(ref T outPoint);


        ///////////////////////////////////////////////////////////
        // REMOVE POINT
        /// <summary>
        /// Removes the specified point
        /// </summary>
        /// <param name="point">the point (in global space) to remove</param>
        public void RemovePoint(T point)
        {
            RemovePoint(point, false);
        }
        /// <summary>
        /// Removes the specified point
        /// </summary>
        /// <param name="point">the point to remove</param>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public void RemovePoint(T point, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
                ToLocalCoordinates(ref point);

            RemovePoint(_Points.IndexOf(point));
        }
        /// <summary>
        /// Removes the point with the specified index
        /// </summary>
        /// <param name="index">the index of the point to remove</param>
        public void RemovePoint(int index)
        {
            if (_Points.Count <= 3)
                throw new TooFewPointsException("A PointList must have at least 3 corners!");

            // Remove a point
            if ((index < _Lines.Count) && (index >= 0))
            {
                _Lines[index].Dispose();
                _Lines.RemoveAt(index);

                int h = index - 1;
                if (h >= 0 || IsClosed)
                {
                    Line line = _Lines[(h + _Lines.Count) % _Lines.Count];
                    Vector2 pointPos = GetVectorOfPoint(_Points[(index + 1 + _Points.Count) % _Points.Count]);
                    line.SetPoints(line._StartPoint, line._Transform.TransformGlobalToLocal(pointPos), true);
                }
            }
            // Remove the last point (not closed PointList)
            else if (index == _Lines.Count)
            {
                _Lines[index - 1].Dispose();
                _Lines.RemoveAt(index - 1);
            }
            else
            {
                throw new IndexOutOfRangeException("There was no Point found at the given index.");
            }

            _Points.RemoveAt(index);

            FitBounding();
            CallOnChange();

        }

        ///////////////////////////////////////////////////////////
        // INSERT POINT
        /// <summary>
        /// Inserts a point by splitting the given line
        /// </summary>
        /// <param name="newPoint">the point to insert</param>
        /// <param name="line">the line to split</param>
        public void InsertPoint(T newPoint, Line line)
        {
            int idx = _Lines.IndexOf(line);
            InsertPointAfter(idx, newPoint);
        }
        ///////////////////////////////////////////////////////////
        // INSERT POINT BEFORE
        /// <summary>
        /// Inserts a point (global space) before another point
        /// </summary>
        /// <param name="referencedPoint">the point after the new point</param>
        /// <param name="newPoint">the point to insert</param>
        public void InsertPointBefore(T referencedPoint, T newPoint)
        {
            InsertPointBefore(referencedPoint, newPoint, false);
        }
        /// <summary>
        /// Inserts a point before another point
        /// </summary>
        /// <param name="referencedPoint">the point after the new point</param>
        /// <param name="newPoint">the point to insert</param>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public void InsertPointBefore(T referencedPoint, T newPoint, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
            {
                ToLocalCoordinates(ref referencedPoint);
                ToLocalCoordinates(ref newPoint);
            }

            int idx = _Points.IndexOf(referencedPoint);

            InsertPointBefore(idx, newPoint, true);
        }

        /// <summary>
        /// Inserts a point (global space) before the point with the given index (the new point will get this index)
        /// </summary>
        /// <param name="index">the index where to insert</param>
        /// <param name="newPoint">the point to insert</param>
        public void InsertPointBefore(int index, T newPoint)
        {
            InsertPointBefore(index, newPoint, false);
        }
        /// <summary>
        /// Inserts a point before the point with the given index (the new point will get this index)
        /// </summary>
        /// <param name="index">the index where to insert</param>
        /// <param name="newPoint">the point to insert</param>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public void InsertPointBefore(int index, T newPoint, bool isLocalCoordinate)
        { 
            if (!isLocalCoordinate)
                ToLocalCoordinates(ref newPoint);

            _Points.Insert(index, newPoint);
            _Lines.Insert(index, new Line(GetVectorOfPoint(_Points[index]), GetVectorOfPoint(_Points[index + 1])));

            if (index != 0 || IsClosed)
            {
                Line line = _Lines[(_Lines.Count + index - 1) % _Lines.Count];
                line.SetPoints(line._StartPoint, line._Transform.TransformGlobalToLocal(_Lines[index].StartPoint), true);
            }

            FitBounding();
            CallOnChange();
        }


        ///////////////////////////////////////////////////////////
        // INSERT POINT AFTER
        /// <summary>
        /// Inserts a point (global space) after another point
        /// </summary>
        /// <param name="referencedPoint">the point before the new point</param>
        /// <param name="newPoint">the point to insert</param>
        public void InsertPointAfter(T referencedPoint, T newPoint)
        {
            InsertPointAfter(referencedPoint, newPoint, false);
        }
        /// <summary>
        /// Inserts a point after another point
        /// </summary>
        /// <param name="referencedPoint">the point before the new point</param>
        /// <param name="newPoint">the point to insert</param>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public void InsertPointAfter(T referencedPoint, T newPoint, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
            {
                ToLocalCoordinates(ref referencedPoint);
                ToLocalCoordinates(ref newPoint);
            }
            int idx = _Points.IndexOf(referencedPoint);
            InsertPointAfter(idx, newPoint, true);
        }
        /// <summary>
        /// Inserts a point (global space) after the point with the given index
        /// </summary>
        /// <param name="index">the index of the point before</param>
        /// <param name="newPoint">the point to insert</param>
        public void InsertPointAfter(int index, T newPoint)
        {
            InsertPointAfter(index, newPoint, false);
        }
        /// <summary>
        /// Inserts a point after the point with the given index
        /// </summary>
        /// <param name="index">the index of the point before</param>
        /// <param name="newPoint">the point to insert</param>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public void InsertPointAfter(int index, T newPoint, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
                ToLocalCoordinates(ref newPoint);

            _Points.Insert(index + 1, newPoint);
            _Lines.Insert(index, new Line(GetVectorOfPoint(_Points[index]), GetVectorOfPoint(newPoint)));

            if (index + 1 != _Points.Count - 1 || IsClosed)
            {
                Line line = _Lines[index + 1];
                line.SetPoints(line._Transform.TransformGlobalToLocal(_Lines[index].EndPoint), line._EndPoint, true);
            }

            FitBounding();
            CallOnChange();
        }


        ///////////////////////////////////////////////////////////
        // EDIT POINT
        /// <summary>
        /// Edit a point (global space)
        /// </summary>
        /// <param name="previousPoint">the position of the point before editing</param>
        /// <param name="newPoint">the position of the point after editing</param>
        public void EditPoint(T previousPoint, T newPoint)
        {
            EditPoint(previousPoint, newPoint, false);
        }
        /// <summary>
        /// Edit a point
        /// </summary>
        /// <param name="previousPoint">the position of the point before editing</param>
        /// <param name="newPoint">the position of the point after editing</param>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public void EditPoint(T previousPoint, T newPoint, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
            {
                ToLocalCoordinates(ref previousPoint);
                ToLocalCoordinates(ref newPoint);
            }
            EditPoint(_Points.IndexOf(previousPoint), newPoint, true);
        }
        /// <summary>
        /// Edit a point (global space)
        /// </summary>
        /// <param name="index">the index of the point to edit</param>
        /// <param name="newPoint">the position of the point after editing</param>
        public void EditPoint(int index, T newPoint)
        {
            EditPoint(index, newPoint, false);
        }
        /// <summary>
        /// Edit a point
        /// </summary>
        /// <param name="index">the index of the point to edit</param>
        /// <param name="newPoint">the position of the point after editing</param>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public void EditPoint(int index, T newPoint, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
                ToLocalCoordinates(ref newPoint);

            _Points[index] = newPoint;

            // change start point of Line[index]
            if (index < _Lines.Count)
            {
                _Lines[index].SetPoints(
                    _Lines[index]._Transform.TransformGlobalToLocal(GetVectorOfPoint(newPoint)),
                    _Lines[index]._EndPoint, 
                    true);
            }
            // change end point of Line[index - 1]
            if (index != 0 || IsClosed)
            {
                Line line = _Lines[(_Lines.Count + index - 1) % _Lines.Count];
                line.SetPoints(
                    line._StartPoint, 
                    line._Transform.TransformGlobalToLocal(GetVectorOfPoint(newPoint)), 
                    true);
            }

            FitBounding();
            CallOnChange();
        }

        /// <summary>
        /// Returns the Index of the given point
        /// </summary>
        /// <param name="point">the point to look after it's index</param>
        /// <returns>the index of the point</returns>
        public int GetIndexOfPoint(T point)
        {
            return _Points.IndexOf(point);
        }

        ///////////////////////////////////////////////////////////
        // GET NEAREST CORNER
        /// <summary>
        /// Returns the nearest point of the PointList (doesn't matter what kind of point)
        /// </summary>
        /// <param name="position">the position (global space) from where the nearest point shall be catched</param>
        /// <returns>the nearest point of the list</returns>
        public T GetNearestCorner(Vector2 position)
        {
            return GetNearestCorner(position, false);
        }
        /// <summary>
        /// Returns the nearest point of the PointList (doesn't matter what kind of point)
        /// </summary>
        /// <param name="position">the position from where the nearest point shall be catched</param>
        /// <returns>the nearest point of the list</returns>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public T GetNearestCorner(Vector2 position, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
                _Transform.TransformGlobalToLocal(ref position);

            T nearest = _Points[0];
            foreach (T point in _Points)
            {
                if (Vector2.DistanceSquared( GetVectorOfPoint( point ) , position )
                    < Vector2.DistanceSquared(GetVectorOfPoint(nearest), position))
                {
                    nearest = point;
                }
            }

            if (isLocalCoordinate)
                return nearest;

            ToGlobalCoordinates(ref nearest);
            return nearest;
        }

     
        ///////////////////////////////////////////////////////////
        // GET NEAREST LINE
        /// <summary>
        /// Returns the nearest line connected between to of the points (doesn't matter what kind of point)
        /// </summary>
        /// <param name="position">the position from where the nearest line shall be catched </param>
        /// <returns>the nearest line</returns>
        public Line GetNearestLine(Vector2 position)
        {
            return GetNearestLine(position, false);
        }
        /// <summary>
        /// Returns the nearest line connected between to of the points (doesn't matter what kind of point)
        /// </summary>
        /// <param name="position">the position from where the nearest line shall be catched </param>
        /// <returns>the nearest line</returns>
        /// <param name="isLocalCoordinate">true, if the passed coordinates are in local space</param>
        public Line GetNearestLine(Vector2 position, bool isLocalCoordinate)
        {
            if (!isLocalCoordinate)
                _Transform.TransformGlobalToLocal(ref position);

            Line nearest = _Lines[0];
            float nearDiSq = float.MaxValue;

            foreach (Line line in _Lines)
            {
                float lineDiSq = Vector2.DistanceSquared( line.GetNearestPointOnEdge( position ), position );

                if (lineDiSq < nearDiSq)
                {
                    nearest = line;
                    nearDiSq = lineDiSq;
                }
            }

            return nearest;
        }
    }
}
